package gsql

import (
	"bytes"
	"database/sql"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"reflect"
	"strconv"
	"time"
)

type FieldType int

const (
	ftUnknown FieldType = iota
	ftInt
	ftUint
	ftString
	ftFloat
	ftBool
	ftTime
	ftBytes
)

type dbConn interface {
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
}

type DBx struct {
	db *sql.DB
}

var glog *log.Logger

func init() {
	glog = log.New(os.Stdout, `gsql>`, log.Lshortfile|log.Ldate|log.Ltime)
}

func New(driverName, dataSourceName string) (dbx *DBx, err error) {
	dbx = new(DBx)
	dbx.db, err = sql.Open(driverName, dataSourceName)
	//glog.Println(err)
	return dbx, err
}

func (this *DBx) Close() error {
	return this.db.Close()
}

func (this *DBx) NewQuery(args ...interface{}) *Query {
	query := new(Query)
	query.conn = this.db
	query.isPrepare = false
	if len(args) > 0 {
		if val, ok := args[0].(bool); ok {
			query.isPrepare = val
		}
	}
	return query
}

func (this *DBx) NewTrans() (t *Transaction, e error) {
	t = new(Transaction)
	t.tx, e = this.db.Begin()
	return t, e
}

type Transaction struct {
	tx *sql.Tx
}

func (this *Transaction) NewQuery(args ...interface{}) *Query {
	query := new(Query)
	query.conn = this.tx
	if len(args) > 0 {
		query.isPrepare = true
	}
	return query
}

func (this *Transaction) Commit() error {
	return this.tx.Commit()
}

func (this *Transaction) Rollback() error {
	return this.tx.Rollback()
}

type Query struct {
	conn            dbConn
	rows            *sql.Rows
	fields          *Fields
	isPrepare       bool
	stmt            *sql.Stmt
	sqlStr          string
	sqlArgs         []interface{}
	sqlArgs_prepare []interface{}
}

func (this *Query) checkError() {

}

func (this *Query) SetSQL(query string, args ...interface{}) *Query {
	this.Close()
	this.sqlStr = query
	this.sqlArgs = args
	return this
}

func (this *Query) GetOne(args ...interface{}) (err error) {
	if err = this.Open(args...); err != nil {
		return err
	}

	if !this.Next() {
		if err = this.rows.Err(); err != nil {
			return err
		}
		return sql.ErrNoRows
	}
	return err
}

func (this *Query) Open(args ...interface{}) (err error) {
	this.sqlArgs_prepare = args
	return this.First()
}

func (this *Query) Exec(args ...interface{}) (ret sql.Result, err error) {
	if this.isPrepare {
		if this.stmt == nil {
			if this.stmt, err = this.conn.Prepare(this.sqlStr); err != nil {
				return nil, err
			}
		}
		return this.stmt.Exec(args...)
	}
	return this.conn.Exec(this.sqlStr, this.sqlArgs...)
}

func (this *Query) Close() error {
	if this.rows == nil {
		return fmt.Errorf(`no connection`)
	}
	defer func() {
		this.rows = nil
		this.stmt = nil
		this.fields = nil
	}()

	if this.stmt != nil {
		this.stmt.Close()
	}
	return this.rows.Close()
}

func (this *Query) First() (err error) {
	if this.rows != nil {
		this.Close()
	}

	if this.isPrepare {
		if this.stmt == nil {
			if this.stmt, err = this.conn.Prepare(this.sqlStr); err != nil {
				return err
			}
		}
		this.rows, err = this.stmt.Query(this.sqlArgs_prepare...)
	} else {
		this.rows, err = this.conn.Query(this.sqlStr, this.sqlArgs...)
	}

	if err == nil {
		this.fields = newFields(this.rows)
	}
	return err
}

func (this *Query) Next() bool {
	this.checkError()
	if !this.rows.Next() {
		return false
	}
	this.rows.Scan(this.fields.vals...)
	return true
}

func (this *Query) FieldCount() int {
	this.checkError()
	return this.fields.fieldCount
}

func (this *Query) FieldById(id int) *Field {
	this.checkError()
	return this.fields.fields[id]
}

func (this *Query) FieldByName(name string) *Field {
	this.checkError()
	if field, ok := this.fields.list[name]; ok {
		return field
	}
	return nil
}

type Field struct {
	fieldName  string
	fieldValue interface{}
	dataType   FieldType

	refValue reflect.Value

	//asInt    *int64
	//asUint   *uint64
	//asFloat  *float64
	//asBool *bool
	//asString *string
	//asBytes  *[]byte
	//asTime *time.Time
	//
	//toInt    *int64
	//toUint   *uint64
	//toFloat  *float64
	//toBool *bool
	//toString *string
	//toBytes  *[]byte
	//toTime *time.Time

	//asErr error
	//toErr error
}

func newField(name string) *Field {
	p := new(Field)
	p.fieldName = name
	return p
}

func (this *Field) getDataType() FieldType {
	this.refValue = reflect.ValueOf(this.fieldValue)
	if !this.refValue.IsValid() {
		return ftUnknown
	}
	switch this.refValue.Kind() {
	case reflect.Int:
		fallthrough
	case reflect.Int8:
		fallthrough
	case reflect.Int16:
		fallthrough
	case reflect.Int32:
		fallthrough
	case reflect.Int64:
		return ftInt
	case reflect.Uint:
		fallthrough
	case reflect.Uint8:
		fallthrough
	case reflect.Uint16:
		fallthrough
	case reflect.Uint32:
		fallthrough
	case reflect.Uint64:
		return ftUint
	case reflect.Float32:
		fallthrough
	case reflect.Float64:
		return ftFloat
	case reflect.String:
		return ftString
	case reflect.Bool:
		return ftBool
	case reflect.Slice:
		return ftBytes
	case reflect.Struct:
		switch this.fieldValue.(type) {
		case time.Time:
			return ftTime
		}
	}
	glog.Println(this.refValue.Kind())
	return ftUnknown
}

func (this *Field) IsNull() bool {
	return this.getDataType() == ftUnknown
}

func (this *Field) AsInt64() (n int64, e error) {
	//if this.asInt != nil {
	//	return *this.asInt, this.asErr
	//}
	n = 0
	switch this.getDataType() {
	case ftInt:
		n = this.refValue.Int()
	case ftUint:
		n = int64(this.ToUint64())
	case ftFloat:
		n = int64(this.ToFloat64())
	case ftBool:
		if this.ToBool() {
			n = 1
		}
	case ftString:
		n, e = strconv.ParseInt(this.ToString(), 10, 64)
	case ftBytes:
		e = binary.Read(bytes.NewBuffer(this.ToBytes()), binary.BigEndian, &n)
	case ftTime:
		n = this.ToTime().UnixNano()
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as int`, this.refValue.Type())
	}
	//this.asInt = &n
	//this.asErr = e
	return n, e
}

func (this *Field) ToInt64() (n int64) {
	n, _ = this.AsInt64()
	return n
}

func (this *Field) AsUint64() (n uint64, e error) {
	//if this.asUint != nil {
	//	return *this.asUint, this.asErr
	//}
	n = 0
	switch this.getDataType() {
	case ftUint:
		n = this.refValue.Uint()
	case ftInt:
		n = uint64(this.ToInt64())
	case ftFloat:
		n = uint64(this.ToFloat64())
	case ftBool:
		if this.ToBool() {
			n = 1
		}
	case ftString:
		n, e = strconv.ParseUint(this.ToString(), 10, 64)
	case ftBytes:
		e = binary.Read(bytes.NewBuffer(this.ToBytes()), binary.BigEndian, &n)
	case ftTime:
		n = uint64(this.ToTime().UnixNano())
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as uint`, this.refValue.Type())
	}
	//this.asUint = &n
	//this.asErr = e
	return n, e
}

func (this *Field) ToUint64() uint64 {
	n, _ := this.AsUint64()
	return n
}

func (this *Field) AsFloat32() (f float32, e error) {
	f64, e := this.AsFloat64()
	return float32(f64), e
}

func (this *Field) ToFloat32() float32 {
	f, _ := this.AsFloat32()
	return f
}

func (this *Field) AsFloat64() (f float64, e error) {
	//if this.asFloat != nil {
	//	return *this.asFloat, nil
	//}
	f = 0
	switch this.getDataType() {
	case ftFloat:
		f = this.refValue.Float()
	case ftInt:
		f = float64(this.ToInt64())
	case ftUint:
		f = float64(this.ToUint64())
	case ftBool:
		if this.ToBool() {
			f = 1
		}
	case ftString:
		f, e = strconv.ParseFloat(this.ToString(), 64)
	case ftBytes:
		e = binary.Read(bytes.NewBuffer(this.ToBytes()), binary.BigEndian, &f)
	case ftTime:
		f = float64(this.ToTime().UnixNano())
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as float`, this.refValue.Type())
	}
	//this.asFloat = &f
	//this.asErr = e
	return f, e
}

func (this *Field) ToFloat64() float64 {
	f, _ := this.AsFloat64()
	return f
}

func (this *Field) AsBytes() (b []byte, e error) {
	//if this.asBytes != nil {
	//	return *this.asBytes, this.asErr
	//}
	buf := bytes.NewBuffer([]byte{})
	switch this.getDataType() {
	case ftBytes:
		b = this.refValue.Bytes()
		buf.Write(b)
	case ftInt:
		e = binary.Write(buf, binary.BigEndian, this.ToInt64())
	case ftUint:
		e = binary.Write(buf, binary.BigEndian, this.ToUint64())
	case ftFloat:
		e = binary.Write(buf, binary.BigEndian, this.ToFloat64())
	case ftBool:
		e = binary.Write(buf, binary.BigEndian, this.ToBool())
	case ftString:
		buf.WriteString(this.ToString())
	case ftTime:
		e = binary.Write(buf, binary.BigEndian, this.ToTime().UnixNano())
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as bytes`, this.refValue.Type())
	}
	//this.asBytes = &b
	//this.asErr = e
	return b, e
}

func (this *Field) ToBytes() []byte {
	b, _ := this.AsBytes()
	return b
}

func (this *Field) AsBool() (b bool, e error) {
	//if this.asBool != nil {
	//	return *this.asBool, this.asErr
	//}
	b = false
	switch this.getDataType() {
	case ftBool:
		b = this.refValue.Bool()
	case ftInt:
		b = this.ToInt64() != 0
	case ftUint:
		b = this.ToUint64() != 0
	case ftFloat:
		b = this.ToFloat64() != 0
	case ftString:
		b, e = strconv.ParseBool(this.ToString())
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as bool`, this.refValue.Type())
	}
	//this.asBool = &b
	//this.asErr = e
	return b, e
}

func (this *Field) ToBool() bool {
	b, _ := this.AsBool()
	return b
}

func (this *Field) AsString() (s string, e error) {
	//if this.asString != nil {
	//	return *this.asString, this.asErr
	//}
	s = ``
	switch this.getDataType() {
	case ftString:
		s = this.refValue.String()
	case ftInt:
		s = strconv.FormatInt(this.refValue.Int(), 10)
	case ftUint:
		s = strconv.FormatUint(this.refValue.Uint(), 10)
	case ftFloat:
		s = strconv.FormatFloat(this.refValue.Float(), 'f', -1, 64)
	case ftBool:
		s = strconv.FormatBool(this.refValue.Bool())
	case ftBytes:
		s = string(this.refValue.Bytes())
	case ftTime:
		s = this.ToTime().String()
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as string`, this.refValue.Type())
	}
	//this.asString = &s
	//this.asErr = e
	return s, e
}

func (this *Field) ToString() (s string) {
	s, _ = this.AsString()
	return s
}

func (this *Field) AsTime() (t time.Time, e error) {
	//if this.asTime != nil {
	//	return *this.asTime, this.asErr
	//}
	t = time.Unix(0, 0)
	switch this.getDataType() {
	case ftTime:
		t = this.fieldValue.(time.Time)
	case ftUnknown:
		e = fmt.Errorf(`field is null`)
	default:
		e = fmt.Errorf(`type [%s] can't as time`, this.refValue.Type())
	}
	//this.asTime = &t
	//this.asErr = e
	return t, e
}

func (this *Field) ToTime() (t time.Time) {
	//if this.toTime != nil {
	//	return *this.toTime
	//}
	t = time.Unix(0, 0)
	switch this.getDataType() {
	case ftTime:
		t = this.fieldValue.(time.Time)
	case ftInt:
		n := this.ToInt64()
		t = time.Unix(n/int64(time.Second), n%int64(time.Second))
	case ftUint:
		n := int64(this.ToUint64())
		t = time.Unix(n/int64(time.Second), n%int64(time.Second))
	case ftFloat:
		n := int64(this.ToFloat64())
		t = time.Unix(n/int64(time.Second), n%int64(time.Second))
	}
	//this.toTime = &t
	return t
}

type Fields struct {
	//rows *sql.Rows
	fieldCount int
	list       map[string]*Field
	fields     []*Field
	vals       []interface{}
}

func newFields(rows *sql.Rows) *Fields {
	p := new(Fields)
	p.list = make(map[string]*Field)
	//p.rows = rows
	names, _ := rows.Columns()
	p.fieldCount = len(names)
	p.fields = make([]*Field, p.fieldCount)
	p.vals = make([]interface{}, p.fieldCount)
	for id, name := range names {
		field := newField(name)
		field.dataType = ftUnknown
		p.list[name] = field
		p.fields[id] = field
		p.vals[id] = &field.fieldValue
	}
	return p
}
